﻿using OpenTK.Graphics;
using OpenTK;
using OpenTK.Math;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Collections.Generic;

using Rekx;

namespace Rekx.Graphics 
{
    /// <summary>
    /// Responsible for all scene drawning.
    /// </summary>
    public class GraphicsDevice : IDisposable
    {
        private struct DrawableMatrixPair
        {
            public DrawableComponent component;
            public Matrix4 matrix;

            public DrawableMatrixPair(DrawableComponent component, Matrix4 matrix)
            {
                this.component = component;
                this.matrix = matrix;
            }
        }

        #region Private variables
        TextureFont font = new TextureFont(new Font(FontFamily.GenericMonospace, 16.0f));
        TextPrinter textPrinter = new TextPrinter();

        private int width = 800;
        private int height = 600;

        List<DrawableMatrixPair> Drawables = new List<DrawableMatrixPair>();

        Camera camera;

        // shading
        ShaderContext shaderContext;
        Effect baseEffect;

        
        #endregion 


        #region Properties
        /// <summary>
        /// Sets the width of the render area in pixels
        /// </summary>
        public int Width
        {
            set 
            {
                if (value > 0)
                    width = value;
            }
        }


        /// <summary>
        /// Sets the height of the render area in pixels
        /// </summary>
        public int Height
        {
            set
            {
                if (value > 0)
                   height = value;
            }
        }

        /// <summary>
        /// Sets the camera to view the scene with
        /// </summary>
        public Camera Camera
        {
            set
            {
                camera = value;
            }
        }

        #endregion 

        /// <summary>
        /// Creates the Graphics device
        /// </summary>
        public GraphicsDevice()
        {
            // enable vertexbuffers in OGL
            GL.EnableClientState(EnableCap.VertexArray);
           // GL.EnableClientState(EnableCap.ColorArray);
            GL.EnableClientState(EnableCap.TextureCoordArray);

            // set background color
            GL.ClearColor(Color.SteelBlue);
            // enable depthtest...
            GL.Enable(EnableCap.DepthTest);            

            // texturing
            GL.Enable(EnableCap.Texture2D);
            GL.Hint(HintTarget.PerspectiveCorrectionHint, HintMode.Nicest);

           
            // initialize shading
            shaderContext = new ShaderContext();
            baseEffect = CreateEffect(Defines.DATADIR + "basic.cgfx");

           
        }
        
       

        /// <summary>
        /// Draws the current scene
        /// </summary>
        /// <param name="ms">Milliseconds between updates</param>
        public void DrawScene(float ms)
        {
            // setup the viewport
            // I'll do this every frame, if it becomes a problem, it should be refactored
            // to a function that only sets it when changes are made; i.e. through events
            GL.Viewport(0, 0, width, height);            
            

            // clear buffers from last frame
            GL.Clear(ClearBufferMask.ColorBufferBit |
                     ClearBufferMask.DepthBufferBit);

            
            // draw all components
            foreach (DrawableMatrixPair pair in Drawables)
            {
                DrawComponent(pair.component,  pair.matrix);
            }


            // clear the list of drawables so that they will not get redrawn each frame
            Drawables.Clear();
                              
        }


        /// <summary>
        /// Draw a string of text at the desired position
        /// </summary>
        /// <param name="s">String to draw</param>
        /// <param name="position">Position to draw the string</param>
        public void DrawString(string s, OpenTK.Math.Vector2 position)
        {
            textPrinter.Begin();
            GL.Color3(Color.Black);
            GL.Translate(position.X, position.Y, 0);
            textPrinter.Draw(s, font);
            textPrinter.End();
        }


        /// <summary>
        /// Draws a drawable component
        /// </summary>
        /// <param name="component">Component to draw</param>
        /// <param name="where">Translation matrix</param>
        public void Draw(DrawableComponent component, Matrix4 where)
        {
            Drawables.Add(new DrawableMatrixPair(component, where));
        }


        
        private void DrawComponent(DrawableComponent c,  Matrix4 where)
        {
            // check whether the component has an effect
            Effect effect;

            if (c.hasEffect == true)
            {
                effect = c.Effect;
            }
            else
            {
                effect = baseEffect;    
            }

            Matrix4 worldView = where * camera.View;
            effect.SetValue("WorldView", ref worldView);

            Matrix4 projectionMatrix = camera.Projection;
            effect.SetValue("Projection", ref projectionMatrix);




            Vector3 globalAmbient = new Vector3(0.2f,0.2f,0);
            Vector3 lightColor = new Vector3(1,1,1)*0.4f;
            Vector3 lightPosition = new Vector3(8,2,0);
            Vector3 eyePosition = new Vector3(0, 12, 6);
            Vector3 Ke = new Vector3(0,0.6f,0);
            Vector3 Ka = new Vector3(0.6f,0.4f,0.7f);
            
            Vector3 Kd = new Vector3(0,0,0);
            Vector3 Ks = new Vector3(1,1,1);
            float shininess = 1f;


            effect.SetValue("globalAmbient", ref globalAmbient);
            effect.SetValue("lightColor", ref lightColor);
            effect.SetValue("lightPosition", ref lightPosition);
            effect.SetValue("eyePosition", ref eyePosition);
            effect.SetValue("Ke", ref Ke);
            effect.SetValue("Ka", ref Ka);
            effect.SetValue("Kd", ref Kd);
            effect.SetValue("Ks", ref Ks);
            effect.SetValue("shininess", ref shininess);

 
            foreach (Pass pass in effect)
            {
                pass.Begin();

                if (c.hasTexture)
                    c.Texture.Bind();
               
                c.Geometry.Bind();         
               
                GL.DrawElements(BeginMode.TriangleStrip, c.Geometry.NumElements, DrawElementsType.UnsignedInt, IntPtr.Zero);


                c.Geometry.UnBind();

                if (c.hasTexture)
                    c.Texture.UnBind();

                pass.End();
            }                     
        }



        /// <summary>
        /// TODO: Remove this in favour of something better
        /// </summary>
        /// <param name="filename">Path tó the shader file</param>
        /// <returns></returns>
        public Effect CreateEffect(string filename)
        {
            return shaderContext.CreateEffectFromFile(filename);
        }

        
        #region IDisposable Pattern
        // taken from msdn

        private bool disposed = false;

        // Implement IDisposable.
        // Do not make this method virtual.
        // A derived class should not be able to override this method.

        /// <summary>
        /// Dispose the resources
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        // Dispose(bool disposing) executes in two distinct scenarios.
        // If disposing equals true, the method has been called directly
        // or indirectly by a user's code. Managed and unmanaged resources
        // can be disposed.
        // If disposing equals false, the method has been called by the
        // runtime from inside the finalizer and you should not reference
        // other objects. Only unmanaged resources can be disposed.
        private void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this.disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    // Dispose managed resources.
                    font.Dispose();
                    shaderContext.Dispose();
                    baseEffect.Dispose();

                    

                }

                // Call the appropriate methods to clean up
                // unmanaged resources here.
                // If disposing is false,
                // only the following code is executed.


                // Note disposing has been done.
                disposed = true;

            }
        }

        #endregion
    }

    
}

namespace Rekx
{
    /// <summary>
    /// Contains important defines
    /// </summary>
    public static class Defines
    {
        /// <summary>
        /// Data directory
        /// </summary>
        public static string DATADIR = "../../../data/";
    }
}